//Copyright (C) 2011 Ron Coleman. Contact: ronncoleman@gmail.com
//
//This library is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either
//version 3 of the License, or (at your option) any later version.
//
//This library is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with this library; if not, write to the Free Software
//Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
package bj.actor
import scala.actors.Actor
import bj.hkeeping.Ok
import scala.collection.mutable.HashMap
import bj.hkeeping.NotOk
import scala.actors.OutputChannel
import bj.util.Logs
import bj.Config
import Actor._
import scala.actors.remote.RemoteActor
import scala.actors.remote.RemoteActor._ 
import scala.actors.remote.Node

case class Bet(player: Int, amt: Double)
case class GameStart(players : List[OutputChannel[Any]])
case class GameOver(payouts : HashMap[Int,Outcome])
case class Arrive(mailbox : OutputChannel[Any], player : Int, betAmt : Double)
case class TableNumber(tid : Int)
case class Go
case class Stop
case class SendMsg(message : String)
case class GetTables
case class TestMessage
case class SendTables(tableArray : Array[Array[AnyRef]])
case class GetTotalEarnings
case class SendTotalEarnings(totalEarnings : Double)
case class RemoveTable(table : Int)
case class AddTable(table : Table)
case class EdTable(table : Int, min : Double, max : Double, dealer : String)
case class AddToBankroll(pid : Int, amt : Double)
case class GetPlayers
case class SendPlayers(playerArray : Array[Array[AnyRef]])
case class SendLogin(playerName : String, bankroll : Double)
case class SendLogout(pid : Int)
case class LoggedIn(pid : Int)

case class JoinHuman(playerID : Int, tableNum : Int)
case class JoinBot(playerID : Int, minBet : Double)
case class JoinedTable(remoteActor : Symbol)

/** This object represents the house as a container of tables. */
object House extends Actor with Logs {
  
  // Must specify a class loader:  the ClassLoader that Scala picks when 
  // serializing/deserializing actors is the one Located on RemoteActor which defaults to null.
  // http://stackoverflow.com/questions/3542360/why-is-setting-the-classloader-necessary-with-scala-remoteactors
  RemoteActor.classLoader = getClass.getClassLoader
  
  var nextId = 0
  var closedTablesEarnings = 0.0
  var HouseStarted = false
  
  debug("starting tables")
  var tables = Config.TablesList
  
  debug("starting players")
  var players = Config.PlayersList;
  
  override def toString = "house("+nextId+")"
  
  /**
   * Gets an Array of Arrays containing table information to send to the GUI that has requested it
   */
  def getTables() : Array[Array[AnyRef]] = {
    debug("Getting tables")
    var tablesArray : Array[Array[AnyRef]] = new Array(tables.size)
    var i = 0
    tables.foreach(t => addTable(t))
     def addTable(t:Table) : Unit = {
      tablesArray(i) = t.toArray()
      i = i + 1
    }
    tablesArray
  }
  
  /**
   * Gets an Array of Arrays containing player information to send to the Eye in the Sky GUI
   */
  def getPlayers() : Array[Array[AnyRef]] = {
    debug("Getting players")
    var playersArray : Array[Array[AnyRef]] = new Array(players.size)
    var i = 0;
    players.foreach(p => addPlayer(p))
    def addPlayer(p:Player) : Unit = {
      playersArray(i) = p.toArray()
      i = i + 1
    }
    playersArray
  }
  
  /**
   * Add a new table to the House and tell it to start. Not quite fully implemented
   */
  def addTable(newTable : Table) : Unit = {
    tables ++= List[Table](newTable)
  }
  
  /** 
   * Remove the specified table from the house after telling it to stop. Not quite fully implemented
   */
  def removeTable(removetid : Int) : Unit = {
    debug("Table id to remove: " + removetid)
    tables = tables.remove(t => t.tableID == removetid)
  }
  
  /**
   * Edit the min, max, and/or dealer of the specified table
   */
  def editTable(edittid : Int, newMin : Double, newMax : Double, newDealer : String) : Unit = {
    debug("Table id to edit: " + edittid)
    val edTab : Table = tables.filter(t => t.tableID == edittid)(0)
    edTab.max = newMax
    edTab.min = newMin
    edTab.dealer = newDealer
  }
  
  /**
   * Add to a player's (human) bankroll in response to the player visiting the ATM
   */
  def addToBankroll(playerid : Int, addAmt : Double) : Unit = {
    debug("Player id to adjust bankroll: " + playerid)
    val targetPlayer : Player = players.filter(p => p.pid == playerid)(0)
    targetPlayer.bankroll_(targetPlayer.bankroll+addAmt)
  }
  
  /**
   * Add a new remote (human) player to the game's list of players
   */
  def addRemotePlayer(playerName : String, playerBankroll : Double) : HumanPlayer = {
    var newPlayer = new HumanPlayer(playerName,playerBankroll)
    players ++= List[Player](newPlayer)
    newPlayer
  }
  
  /**
   * Remove the specified player from the game
   */
  def removePlayer(removePID : Int) : Unit = {
    debug("PID to remove: " + removePID)
    players = players.remove(p => p.pid == removePID)
  }
  
  /**
   * Calculate the total earnings/losses from all tables (open and closed) from the casino. Not quite fully implemented
   * (closed tables totals are not added)
   */
  def earnings : Double = {
    tables.foldLeft(closedTablesEarnings)((total,thisTable) => total + thisTable.tableEarnings)
  }
  

   
  /** This method receives messages. */
  def act {
    /** Start all players */
  Player.start(players)
  
 // Declare the port I'm listening on
  alive(Config.PortNumber)

// Register me
   register(Config.HouseServiceName, self)

   debug(">>> started "+Config.HouseServiceName+" @ "+Config.IPAddress+":"+Config.PortNumber)

    loop {
      react {
        // Receives a bet from a player and matches it
        // to a table.
        // TODO: Change to AssignToTable; this should cause the player (human or Bot) to be assigned to the given table
        // Should send a message to the player containing the table id 
        case Bet(pid : Int, bet : Double) =>
          debug("house: received bet amt = "+bet)
          
          tables.find(t => t.bets.size < Table.MAX_PLAYERS && t.minBet <= bet) match {
            case None =>
              sender ! NotOk
              
            case Some(table) =>
              debug("house: sending table id = "+table.tid+" sender = "+sender)
              table ! Arrive(sender, pid, bet)
              
              sender ! TableNumber(table.tid)
          }
          
        // Receives a message to tell the tables to go 
        case Go =>
          debug("house: receive Go for "+tables.size+" tables")
          HouseStarted = true
          tables.foreach(t => t ! Go)
          
        // Receives a message to tell the tables to stop
        case Stop =>
           debug("house: received Stop")
           HouseStarted = false
           tables.foreach(t => t ! Stop) 
          
        // Test only
        case TestMessage => 
          debug("House received TestMessage")
          sender ! SendMsg("Test Message")
          
        
        // Receive a message to get table information
        case GetTables =>
          debug("House received GetTables")
          if (HouseStarted) {
          sender ! SendTables(getTables())
          sender ! SendTotalEarnings(earnings)
          }
          else
          {
            debug("Did not send table information because house is not started")
          }
        
        // Receive a message to get player information
        case GetPlayers =>
          debug("House received GetPlayers")
          sender ! SendPlayers(getPlayers())
        
        // Receive a message to remove the specified table number
        case RemoveTable(tableNum) => 
          debug("House received Remove for table "+tableNum)
          tables.find(t => t.tableID == tableNum) match {
            case None =>
              sender ! NotOk
              
            case Some(table) =>
              debug("house: sending stop to table "+tableNum)
              table ! Stop
          }
          removeTable(tableNum)
          sender ! SendTables(getTables())
          sender ! SendTotalEarnings(earnings)
          
        // Receive a message to add a new table to the house  
        case AddTable(table) =>
          debug("House received Add for table "+table.tableID)
          addTable(table)
          sender ! SendTables(getTables())
          sender ! SendTotalEarnings(earnings)
        
        // Receive a message to edit the specified table. Can edit min bet, max bet, or dealer  
        case EdTable(tableNum, min, max, dealer) =>
          debug("House received EditTable for table "+tableNum)
          editTable(tableNum,min,max,dealer)
          sender ! SendTables(getTables())
          sender ! SendTotalEarnings(earnings)
        
        // Receive a message to add the given playerid to the given table. The playerid represents a
        // human player
        case JoinHuman(playerid, tableNum) =>
          //find out if the player can join the table
          //assign the table to the player (and vice versa)
         tables.find(t => t.tableID == tableNum) match {
            case None =>
              sender ! NotOk
              
            case Some(table) =>
              players.find(p => p.pid == playerid) match {
                case None =>
                  sender ! NotOk
                case Some(player) =>
                  player ! AssignToTable(sender,table,0)
              }
              
          sender ! SendTables(getTables())
          }
         
        // Receive a message to log a new human player in to the system.  Creates a new instance of HumanPlayer 
        // and starts it.
        case SendLogin(name,bankroll) => 
          debug("House received login for player = "+name+", bankroll = "+bankroll)
          val newPlayer = addRemotePlayer(name,bankroll)
          newPlayer.start
          sender ! LoggedIn(newPlayer.pid)
          
        // Receive a message to log a human player out of the system.  Removes the specified player from the game.    
        case SendLogout(pid) =>
          debug("House received logout for pid = "+pid)
          removePlayer(pid)
          
        // Receive an indication that a human player has visited the ATM and added funds to his or her bankroll  
        case AddToBankroll(pid,amt) => addToBankroll(pid,amt)  
          
        // Receives something completely from left field
        case dontKnow =>
          // Got something we REALLY didn't expect
          debug(this+" got "+dontKnow)          
        }
    }
  } 
}